/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.iotdb.confignode.procedure;

import org.apache.iotdb.confignode.procedure.exception.*;
import org.apache.iotdb.confignode.procedure.state.ProcedureLockState;
import org.apache.iotdb.confignode.procedure.state.ProcedureState;
import org.apache.iotdb.confignode.procedure.store.IProcedureStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * Abstract class of all procedures.
 *
 * @param <Env>
 */
public abstract class Procedure<Env> implements Comparable<Procedure<Env>> {
    private static final Logger LOG = LoggerFactory.getLogger(Procedure.class);
    public static final long NO_PROC_ID = -1;
    public static final long NO_TIMEOUT = -1;

    private long parentProcId = NO_PROC_ID;
    private long rootProcId = NO_PROC_ID;
    private long procId = NO_PROC_ID;
    private long submittedTime;

    private ProcedureState state = ProcedureState.INITIALIZING;
    private int childrenLatch = 0;
    private ProcedureException exception;

    private volatile long timeout = NO_TIMEOUT;
    private volatile long lastUpdate;

    private volatile byte[] result = null;
    private volatile boolean locked = false;
    private boolean lockedWhenLoading = false;

    private int[] stackIndexes = null;

    private boolean persist = true;

    public boolean needPersistance() {
        return this.persist;
    }

    public void resetPersistance() {
        this.persist = true;
    }

    public final void skipPersistance() {
        this.persist = false;
    }

    public final boolean hasLock() {
        return locked;
    }

    // User level code, override it if necessary

    /**
     * The main code of the procedure. It must be idempotent since execute() may be called multiple
     * times in case of machine failure in the middle of the execution.
     *
     * @param env the environment passed to the ProcedureExecutor
     * @return a set of sub-procedures to run or ourselves if there is more work to do or null if the
     * procedure is done.
     * @throws ProcedureYieldException     the procedure will be added back to the queue and retried
     *                                     later.
     * @throws InterruptedException        the procedure will be added back to the queue and retried later.
     * @throws ProcedureSuspendedException Signal to the executor that Procedure has suspended itself
     *                                     and has set itself up waiting for an external event to wake it back up again.
     */
    protected abstract Procedure<Env>[] execute(Env env)
            throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException;

    /**
     * The code to undo what was done by the execute() code. It is called when the procedure or one of
     * the sub-procedures failed or an abort was requested. It should cleanup all the resources
     * created by the execute() call. The implementation must be idempotent since rollback() may be
     * called multiple time in case of machine failure in the middle of the execution.
     *
     * @param env the environment passed to the ProcedureExecutor
     * @throws IOException          temporary failure, the rollback will retry later
     * @throws InterruptedException the procedure will be added back to the queue and retried later
     */
    protected abstract void rollback(Env env)
            throws IOException, InterruptedException, ProcedureException;

    /**
     * The abort() call is asynchronous and each procedure must decide how to deal with it, if they
     * want to be abortable. The simplest implementation is to have an AtomicBoolean set in the
     * abort() method and then the execute() will check if the abort flag is set or not. abort() may
     * be called multiple times from the client, so the implementation must be idempotent.
     *
     * <p>NOTE: abort() is not like Thread.interrupt(). It is just a notification that allows the
     * procedure implementor abort.
     */
    protected abstract boolean abort(Env env);

    public void serialize(DataOutputStream stream) throws IOException {
        // procid
        stream.writeLong(this.procId);
        // state
        stream.writeInt(this.state.ordinal());
        // submit time
        stream.writeLong(this.submittedTime);
        // last updated
        stream.writeLong(this.lastUpdate);
        // parent id
        stream.writeLong(this.parentProcId);
        // time out
        stream.writeLong(this.timeout);
        // stack indexes
        if (stackIndexes != null) {
            stream.writeInt(stackIndexes.length);
            for (int index : stackIndexes) {
                stream.writeInt(index);
            }
        } else {
            stream.writeInt(-1);
        }

        // exceptions
        if (hasException()) {
            stream.write((byte) 1);
            String exceptionClassName = exception.getClass().getName();
            byte[] exceptionClassNameBytes = exceptionClassName.getBytes(StandardCharsets.UTF_8);
            stream.writeInt(exceptionClassNameBytes.length);
            stream.write(exceptionClassNameBytes);
            String message = this.exception.getMessage();
            if (message != null) {
                byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
                stream.writeInt(messageBytes.length);
                stream.write(messageBytes);
            } else {
                stream.writeInt(-1);
            }
        } else {
            stream.write((byte) 0);
        }

        // result
        if (result != null) {
            stream.writeInt(result.length);
            stream.write(result);
        } else {
            stream.writeInt(-1);
        }

        // has lock
        stream.write(this.hasLock() ? (byte) 1 : (byte) 0);
    }

    public void deserialize(ByteBuffer byteBuffer) {
        // procid
        this.setProcId(byteBuffer.getLong());
        // state
        this.setState(ProcedureState.values()[byteBuffer.getInt()]);
        //  submit time
        this.setSubmittedTime(byteBuffer.getLong());
        //  last updated
        this.setLastUpdate(byteBuffer.getLong());
        //  parent id
        this.setParentProcId(byteBuffer.getLong());
        //  time out
        this.setTimeout(byteBuffer.getLong());
        //  stack index
        int stackIndexesLen = byteBuffer.getInt();
        if (stackIndexesLen >= 0) {
            List<Integer> indexList = new ArrayList<>(stackIndexesLen);
            for (int i = 0; i < stackIndexesLen; i++) {
                indexList.add(byteBuffer.getInt());
            }
            this.setStackIndexes(indexList);
        }
        // exceptions
        if (byteBuffer.get() == 1) {
            Class<?> exceptionClass = deserializeTypeInfo(byteBuffer);
            int messageBytesLength = byteBuffer.getInt();
            String errMsg = null;
            if (messageBytesLength > 0) {
                byte[] messageBytes = new byte[messageBytesLength];
                byteBuffer.get(messageBytes);
                errMsg = new String(messageBytes, StandardCharsets.UTF_8);
            }
            ProcedureException exception;
            try {
                exception =
                        (ProcedureException) exceptionClass.getConstructor(String.class).newInstance(errMsg);
            } catch (InstantiationException
                    | IllegalAccessException
                    | InvocationTargetException
                    | NoSuchMethodException e) {
                LOG.warn("Instantiation exception class failed", e);
                exception = new ProcedureException(errMsg);
            }

            setFailure(exception);
        }

        // result
        int resultLen = byteBuffer.getInt();
        if (resultLen > 0) {
            byte[] resultArr = new byte[resultLen];
            byteBuffer.get(resultArr);
        }
        //  has  lock
        if (byteBuffer.get() == 1) {
            this.lockedWhenLoading();
        }
    }

    /**
     * Deserialize class Name and load class
     *
     * @param byteBuffer bytebuffer
     * @return Procedure
     */
    public static Class<?> deserializeTypeInfo(ByteBuffer byteBuffer) {
        int classNameBytesLen = byteBuffer.getInt();
        byte[] classNameBytes = new byte[classNameBytesLen];
        byteBuffer.get(classNameBytes);
        String className = new String(classNameBytes, StandardCharsets.UTF_8);
        Class<?> clazz;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Invalid procedure class", e);
        }
        return clazz;
    }

    public static Procedure<?> newInstance(ByteBuffer byteBuffer) {
        Class<?> procedureClass = deserializeTypeInfo(byteBuffer);
        Procedure<?> procedure;
        try {
            procedure = (Procedure<?>) procedureClass.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Instantiation failed", e);
        }
        return procedure;
    }

    /**
     * The {@link #doAcquireLock(Object, IProcedureStore)} will be split into two steps, first, it
     * will call us to determine whether we need to wait for initialization, second, it will call
     * {@link #acquireLock(Object)} to actually handle the lock for this procedure.
     *
     * @return true means we need to wait until the environment has been initialized, otherwise true.
     */
    protected boolean waitInitialized(Env env) {
        return false;
    }

    /**
     * Acquire a lock, user should override it if necessary.
     *
     * @param env environment
     * @return state of lock
     */
    protected ProcedureLockState acquireLock(Env env) {
        return ProcedureLockState.LOCK_ACQUIRED;
    }

    /**
     * Release a lock, user should override it if necessary.
     *
     * @param env env
     */
    protected void releaseLock(Env env) {
        // no op
    }

    /**
     * Used to keep procedure lock even when the procedure is yielded or suspended.
     *
     * @param env env
     * @return true if hold the lock
     */
    protected boolean holdLock(Env env) {
        return false;
    }

    /**
     * Called before the procedure is recovered and added into the queue.
     *
     * @param env environment
     */
    protected final void beforeRecover(Env env) {
        // no op
    }

    /**
     * Called when the procedure is recovered and added into the queue.
     *
     * @param env environment
     */
    protected final void afterRecover(Env env) {
        // no op
    }

    /**
     * Called when the procedure is completed (success or rollback). The procedure may use this method
     * to clean up in-memory states. This operation will not be retried on failure.
     *
     * @param env environment
     */
    protected void completionCleanup(Env env) {
        // no op
    }

    /**
     * To make executor yield between each execution step to give other procedures a chance to run.
     *
     * @param env environment
     * @return return true if yield is allowed.
     */
    protected boolean isYieldAfterExecution(Env env) {
        return false;
    }

    // -------------------------Internal methods - called by the procedureExecutor------------------

    /**
     * Internal method called by the ProcedureExecutor that starts the user-level code execute().
     *
     * @param env execute environment
     * @return sub procedures
     */
    protected Procedure<Env>[] doExecute(Env env)
            throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException {
        try {
            updateTimestamp();
            return execute(env);
        } finally {
            updateTimestamp();
        }
    }

    /**
     * Internal method called by the ProcedureExecutor that starts the user-level code rollback().
     *
     * @param env execute environment
     * @throws IOException          ioe
     * @throws InterruptedException interrupted exception
     */
    public void doRollback(Env env) throws IOException, InterruptedException, ProcedureException {
        try {
            updateTimestamp();
            rollback(env);
        } finally {
            updateTimestamp();
        }
    }

    /**
     * Internal method called by the ProcedureExecutor that starts the user-level code acquireLock().
     *
     * @param env   environment
     * @param store ProcedureStore
     * @return ProcedureLockState
     */
    public final ProcedureLockState doAcquireLock(Env env, IProcedureStore store) {
        if (waitInitialized(env)) {
            return ProcedureLockState.LOCK_EVENT_WAIT;
        }
        if (lockedWhenLoading) {
            lockedWhenLoading = false;
            locked = true;
            return ProcedureLockState.LOCK_ACQUIRED;
        }
        ProcedureLockState state = acquireLock(env);
        if (state == ProcedureLockState.LOCK_ACQUIRED) {
            locked = true;
            store.update(this);
        }
        return state;
    }

    /**
     * Presist lock state of the procedure
     *
     * @param env   environment
     * @param store ProcedureStore
     */
    public final void doReleaseLock(Env env, IProcedureStore store) {
        locked = false;
        if (getState() != ProcedureState.ROLLEDBACK) {
            store.update(this);
        }
        releaseLock(env);
    }

    public final void restoreLock(Env env) {
        if (!lockedWhenLoading) {
            LOG.debug("{} didn't hold the lock before restarting, skip acquiring lock", this);
            return;
        }
        if (isFinished()) {
            LOG.debug("{} is already bypassed, skip acquiring lock.", this);
            return;
        }
        if (getState() == ProcedureState.WAITING && !holdLock(env)) {
            LOG.debug("{} is in WAITING STATE, and holdLock= false , skip acquiring lock.", this);
            return;
        }
        LOG.debug("{} held the lock before restarting, call acquireLock to restore it.", this);
        acquireLock(env);
    }

    @Override
    public String toString() {
        // Return the simple String presentation of the procedure.
        return toStringSimpleSB().toString();
    }

    /**
     * Build the StringBuilder for the simple form of procedure string.
     *
     * @return the StringBuilder
     */
    protected StringBuilder toStringSimpleSB() {
        final StringBuilder sb = new StringBuilder();

        sb.append("pid=");
        sb.append(getProcId());

        if (hasParent()) {
            sb.append(", ppid=");
            sb.append(getParentProcId());
        }

    /*
     * TODO
     * Enable later when this is being used.
     * Currently owner not used.
    if (hasOwner()) {
      sb.append(", owner=");
      sb.append(getOwner());
    }*/

        sb.append(", state="); // pState for Procedure State as opposed to any other kind.
        toStringState(sb);

        // Only print out locked if actually locked. Most of the time it is not.
        if (this.locked) {
            sb.append(", locked=").append(locked);
        }
        if (hasException()) {
            sb.append(", exception=" + getException());
        }

        sb.append("; ");
        toStringClassDetails(sb);

        return sb;
    }

    /**
     * Extend the toString() information with more procedure details
     */
    public String toStringDetails() {
        final StringBuilder sb = toStringSimpleSB();

        sb.append(" submittedTime=");
        sb.append(getSubmittedTime());

        sb.append(", lastUpdate=");
        sb.append(getLastUpdate());

        final int[] stackIndices = getStackIndexes();
        if (stackIndices != null) {
            sb.append("\n");
            sb.append("stackIndexes=");
            sb.append(Arrays.toString(stackIndices));
        }

        return sb.toString();
    }

    protected String toStringClass() {
        StringBuilder sb = new StringBuilder();
        toStringClassDetails(sb);
        return sb.toString();
    }

    /**
     * Called from {@link #toString()} when interpolating {@link Procedure} State. Allows decorating
     * generic Procedure State with Procedure particulars.
     *
     * @param builder Append current {@link ProcedureState}
     */
    protected void toStringState(StringBuilder builder) {
        builder.append(getState());
    }

    /**
     * Extend the toString() information with the procedure details e.g. className and parameters
     *
     * @param builder the string builder to use to append the proc specific information
     */
    protected void toStringClassDetails(StringBuilder builder) {
        builder.append(getClass().getName());
    }

    // ==========================================================================
    //  Those fields are unchanged after initialization.
    //
    //  Each procedure will get created from the user or during
    //  ProcedureExecutor.start() during the load() phase and then submitted
    //  to the executor. these fields will never be changed after initialization
    // ==========================================================================
    public long getProcId() {
        return procId;
    }

    public boolean hasParent() {
        return parentProcId != NO_PROC_ID;
    }

    public long getParentProcId() {
        return parentProcId;
    }

    public long getRootProcId() {
        return rootProcId;
    }

    public String getProcName() {
        return toStringClass();
    }

    public long getSubmittedTime() {
        return submittedTime;
    }

    /**
     * Called by the ProcedureExecutor to assign the ID to the newly created procedure.
     */
    protected void setProcId(long procId) {
        this.procId = procId;
    }

    public void setProcRunnable() {
        this.submittedTime = System.currentTimeMillis();
        setState(ProcedureState.RUNNABLE);
    }

    /**
     * Called by the ProcedureExecutor to assign the parent to the newly created procedure.
     */
    protected void setParentProcId(long parentProcId) {
        this.parentProcId = parentProcId;
    }

    protected void setRootProcId(long rootProcId) {
        this.rootProcId = rootProcId;
    }

    /**
     * Called on store load to initialize the Procedure internals after the creation/deserialization.
     */
    protected void setSubmittedTime(long submittedTime) {
        this.submittedTime = submittedTime;
    }

    // ==========================================================================
    //  runtime state - timeout related
    // ==========================================================================

    /**
     * @param timeout timeout interval in msec
     */
    protected void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public boolean hasTimeout() {
        return timeout != NO_TIMEOUT;
    }

    /**
     * @return the timeout in msec
     */
    public long getTimeout() {
        return timeout;
    }

    /**
     * Called on store load to initialize the Procedure internals after the creation/deserialization.
     */
    protected void setLastUpdate(long lastUpdate) {
        this.lastUpdate = lastUpdate;
    }

    /**
     * Called by ProcedureExecutor after each time a procedure step is executed.
     */
    protected void updateTimestamp() {
        this.lastUpdate = System.currentTimeMillis();
    }

    public long getLastUpdate() {
        return lastUpdate;
    }

    /**
     * Timeout of the next timeout. Called by the ProcedureExecutor if the procedure has timeout set
     * and the procedure is in the waiting queue.
     *
     * @return the timestamp of the next timeout.
     */
    protected long getTimeoutTimestamp() {
        return getLastUpdate() + getTimeout();
    }

    // ==========================================================================
    //  runtime state
    // ==========================================================================

    /**
     * @return the time elapsed between the last update and the start time of the procedure.
     */
    public long elapsedTime() {
        return getLastUpdate() - getSubmittedTime();
    }

    /**
     * @return the serialized result if any, otherwise null
     */
    public byte[] getResult() {
        return result;
    }

    /**
     * The procedure may leave a "result" on completion.
     *
     * @param result the serialized result that will be passed to the client
     */
    protected void setResult(byte[] result) {
        this.result = result;
    }

    /**
     * Will only be called when loading procedures from procedure store, where we need to record
     * whether the procedure has already held a lock. Later we will call {@link #restoreLock(Object)}
     * to actually acquire the lock.
     */
    final void lockedWhenLoading() {
        this.lockedWhenLoading = true;
    }

    /**
     * Can only be called when restarting, before the procedure actually being executed, as after we
     * actually call the {@link #doAcquireLock(Object, IProcedureStore)} method, we will reset {@link
     * #lockedWhenLoading} to false.
     *
     * <p>Now it is only used in the ProcedureScheduler to determine whether we should put a Procedure
     * in front of a queue.
     */
    public boolean isLockedWhenLoading() {
        return lockedWhenLoading;
    }

    // ==============================================================================================
    //  Runtime state, updated every operation by the ProcedureExecutor
    //
    //  There is always 1 thread at the time operating on the state of the procedure.
    //  The ProcedureExecutor may check and set states, or some Procecedure may
    //  update its own state. but no concurrent updates. we use synchronized here
    //  just because the procedure can get scheduled on different executor threads on each step.
    // ==============================================================================================

    /**
     * @return true if the procedure is in a RUNNABLE state.
     */
    public synchronized boolean isRunnable() {
        return state == ProcedureState.RUNNABLE;
    }

    public synchronized boolean isInitializing() {
        return state == ProcedureState.INITIALIZING;
    }

    /**
     * @return true if the procedure has failed. It may or may not have rolled back.
     */
    public synchronized boolean isFailed() {
        return state == ProcedureState.FAILED || state == ProcedureState.ROLLEDBACK;
    }

    /**
     * @return true if the procedure is finished successfully.
     */
    public synchronized boolean isSuccess() {
        return state == ProcedureState.SUCCESS && !hasException();
    }

    /**
     * @return true if the procedure is finished. The Procedure may be completed successfully or
     * rolledback.
     */
    public synchronized boolean isFinished() {
        return isSuccess() || state == ProcedureState.ROLLEDBACK;
    }

    /**
     * @return true if the procedure is waiting for a child to finish or for an external event.
     */
    public synchronized boolean isWaiting() {
        switch (state) {
            case WAITING:
            case WAITING_TIMEOUT:
                return true;
            default:
                break;
        }
        return false;
    }

    protected synchronized void setState(final ProcedureState state) {
        this.state = state;
        updateTimestamp();
    }

    public synchronized ProcedureState getState() {
        return state;
    }

    protected synchronized void setFailure(final String source, final Throwable cause) {
        setFailure(new ProcedureException(source, cause));
    }

    protected synchronized void setFailure(final ProcedureException exception) {
        this.exception = exception;
        if (!isFinished()) {
            setState(ProcedureState.FAILED);
        }
    }

    protected void setAbortFailure(final String source, final String msg) {
        setFailure(source, new ProcedureAbortedException(msg));
    }

    /**
     * Called by the ProcedureExecutor when the timeout set by setTimeout() is expired.
     *
     * <p>Another usage for this method is to implement retrying. A procedure can set the state to
     * {@code WAITING_TIMEOUT} by calling {@code setState} method, and throw a {@link
     * ProcedureSuspendedException} to halt the execution of the procedure, and do not forget a call
     * {@link #setTimeout(long)} method to set the timeout. And you should also override this method
     * to wake up the procedure, and also return false to tell the ProcedureExecutor that the timeout
     * event has been handled.
     *
     * @return true to let the framework handle the timeout as abort, false in case the procedure
     * handled the timeout itself.
     */
    protected synchronized boolean setTimeoutFailure(Env env) {
        if (state == ProcedureState.WAITING_TIMEOUT) {
            long timeDiff = System.currentTimeMillis() - lastUpdate;
            setFailure(
                    "ProcedureExecutor",
                    new ProcedureTimeoutException("Operation timed out after " + timeDiff + " ms."));
            return true;
        }
        return false;
    }

    public synchronized boolean hasException() {
        return exception != null;
    }

    public synchronized ProcedureException getException() {
        return exception;
    }

    /**
     * Called by the ProcedureExecutor on procedure-load to restore the latch state
     */
    protected synchronized void setChildrenLatch(int numChildren) {
        this.childrenLatch = numChildren;
        if (LOG.isTraceEnabled()) {
            LOG.trace("CHILD LATCH INCREMENT SET " + this.childrenLatch, new Throwable(this.toString()));
        }
    }

    /**
     * Called by the ProcedureExecutor on procedure-load to restore the latch state
     */
    protected synchronized void incChildrenLatch() {
        // TODO: can this be inferred from the stack? I think so...
        this.childrenLatch++;
        if (LOG.isTraceEnabled()) {
            LOG.trace("CHILD LATCH INCREMENT " + this.childrenLatch, new Throwable(this.toString()));
        }
    }

    /**
     * Called by the ProcedureExecutor to notify that one of the sub-procedures has completed.
     */
    private synchronized boolean childrenCountDown() {
        assert childrenLatch > 0 : this;
        boolean b = --childrenLatch == 0;
        if (LOG.isTraceEnabled()) {
            LOG.trace("CHILD LATCH DECREMENT " + childrenLatch, new Throwable(this.toString()));
        }
        return b;
    }

    /**
     * Try to set this procedure into RUNNABLE state. Succeeds if all subprocedures/children are done.
     *
     * @return True if we were able to move procedure to RUNNABLE state.
     */
    synchronized boolean tryRunnable() {
        // Don't use isWaiting in the below; it returns true for WAITING and WAITING_TIMEOUT
        if (getState() == ProcedureState.WAITING && childrenCountDown()) {
            setState(ProcedureState.RUNNABLE);
            return true;
        } else {
            return false;
        }
    }

    protected synchronized boolean hasChildren() {
        return childrenLatch > 0;
    }

    protected synchronized int getChildrenLatch() {
        return childrenLatch;
    }

    /**
     * Called by the RootProcedureState on procedure execution. Each procedure store its stack-index
     * positions.
     */
    protected synchronized void addStackIndex(final int index) {
        if (stackIndexes == null) {
            stackIndexes = new int[]{index};
        } else {
            int count = stackIndexes.length;
            stackIndexes = Arrays.copyOf(stackIndexes, count + 1);
            stackIndexes[count] = index;
        }
    }

    protected synchronized boolean removeStackIndex() {
        if (stackIndexes != null && stackIndexes.length > 1) {
            stackIndexes = Arrays.copyOf(stackIndexes, stackIndexes.length - 1);
            return false;
        } else {
            stackIndexes = null;
            return true;
        }
    }

    /**
     * Called on store load to initialize the Procedure internals after the creation/deserialization.
     */
    protected synchronized void setStackIndexes(final List<Integer> stackIndexes) {
        this.stackIndexes = new int[stackIndexes.size()];
        for (int i = 0; i < this.stackIndexes.length; ++i) {
            this.stackIndexes[i] = stackIndexes.get(i);
        }
    }

    protected synchronized boolean wasExecuted() {
        return stackIndexes != null;
    }

    protected synchronized int[] getStackIndexes() {
        return stackIndexes;
    }

    /**
     * Helper to lookup the root Procedure ID given a specified procedure.
     */
    protected static long getRootProcedureId(Map<Long, Procedure> procedures, Procedure proc) {
        while (proc.hasParent()) {
            proc = procedures.get(proc.getParentProcId());
            if (proc == null) {
                return NO_PROC_ID;
            }
        }
        return proc.getProcId();
    }

    public void setRootProcedureId(long rootProcedureId) {
        this.rootProcId = rootProcedureId;
    }

    /**
     * @param a the first procedure to be compared.
     * @param b the second procedure to be compared.
     * @return true if the two procedures have the same parent
     */
    public static boolean haveSameParent(Procedure<?> a, Procedure<?> b) {
        return a.hasParent() && b.hasParent() && (a.getParentProcId() == b.getParentProcId());
    }

    @Override
    public int compareTo(Procedure<Env> other) {
        return Long.compare(getProcId(), other.getProcId());
    }
}
